﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;

namespace Aeon.Emulator.Sound.Blaster
{
    /// <summary>
    /// Emulates a Sound Blaster 16 device.
    /// </summary>
    public sealed class SoundBlaster : IInputPort, IOutputPort, IDmaDevice8, IDmaDevice16
    {
        #region Constructors
        /// <summary>
        /// Initializes a new instance of the SoundBlaster class.
        /// </summary>
        /// <param name="vm">Virtual machine instance associated with the device.</param>
        /// <param name="hwnd">Main application window handle.</param>
        /// <param name="irq">IRQ number for the Sound Blaster.</param>
        /// <param name="dma8">8-bit DMA channel for the Sound Blaster.</param>
        /// <param name="dma16">16-bit DMA channel for the Sound Blaster.</param>
        public SoundBlaster(VirtualMachine vm, IntPtr hwnd, int irq, int dma8, int dma16)
        {
            if(vm == null)
                throw new ArgumentNullException("vm");
            if(hwnd == IntPtr.Zero)
                throw new ArgumentNullException("hwnd");

            this.vm = vm;
            this.hwnd = hwnd;
            this.irq = irq;
            this.dma8 = dma8;
            this.dma16 = dma16;
            this.mixer = new Mixer(this);
            this.dmaChannel = vm.DmaController.Channels[this.dma8];
            this.dsp = new Dsp(vm, dma8, dma16);
            this.dsp.AutoInitBufferComplete += (o, e) => RaiseInterrupt();
            this.playbackThread = new Thread(this.AudioPlayback);
            this.playbackThread.IsBackground = true;
            this.playbackThread.Priority = ThreadPriority.AboveNormal;
            this.playbackThread.Start();
        }
        /// <summary>
        /// Initializes a new instance of the SoundBlaster class.
        /// </summary>
        /// <param name="vm">Virtual machine instance associated with the device.</param>
        /// <param name="hwnd">Main application window handle.</param>
        public SoundBlaster(VirtualMachine vm, IntPtr hwnd)
            : this(vm, hwnd, 7, 1, 5)
        {
        }
        /// <summary>
        /// Initializes static fields on the SoundBlaster class.
        /// </summary>
        static SoundBlaster()
        {
            commandLengths.Add(Commands.SetTimeConstant, 1);
            commandLengths.Add(Commands.SingleCycleDmaOutput8, 2);
            commandLengths.Add(Commands.DspIdentification, 1);
            commandLengths.Add(Commands.SetBlockTransferSize, 2);
            commandLengths.Add(Commands.SetSampleRate, 2);
            commandLengths.Add(Commands.SetInputSampleRate, 2);
            commandLengths.Add(Commands.SingleCycleDmaOutput16, 3);
            commandLengths.Add(Commands.AutoInitDmaOutput16, 3);
            commandLengths.Add(Commands.SingleCycleDmaOutput16Fifo, 3);
            commandLengths.Add(Commands.AutoInitDmaOutput16Fifo, 3);
            commandLengths.Add(Commands.SingleCycleDmaOutput8_Alt, 3);
            commandLengths.Add(Commands.AutoInitDmaOutput8_Alt, 3);
            commandLengths.Add(Commands.SingleCycleDmaOutput8Fifo_Alt, 3);
            commandLengths.Add(Commands.AutoInitDmaOutput8Fifo_Alt, 3);
            commandLengths.Add(Commands.PauseForDuration, 2);
            commandLengths.Add(Commands.SingleCycleDmaOutputADPCM4Ref, 2);
            commandLengths.Add(Commands.SingleCycleDmaOutputADPCM2Ref, 2);
            commandLengths.Add(Commands.SingleCycleDmaOutputADPCM3Ref, 2);
        }
        #endregion

        #region Public Properties
        /// <summary>
        /// Gets the hardware IRQ assigned to the device.
        /// </summary>
        public int IRQ
        {
            get { return this.irq; }
        }
        /// <summary>
        /// Gets the DMA channel assigned to the device.
        /// </summary>
        public int DMA
        {
            get { return this.dma8; }
        }
        /// <summary>
        /// Gets the size of the playback buffer in bytes.
        /// </summary>
        public int BufferSize
        {
            get
            {
                var buffer = this.currentSoundBuffer;
                if(buffer == null)
                    return 0;
                return buffer.BufferSize;
            }
        }
        #endregion

        #region IInputPort Members
        IEnumerable<int> IInputPort.InputPorts
        {
            get { return new int[] { Ports.DspReadData, Ports.DspWrite, Ports.DspReadBufferStatus, Ports.MixerAddress, Ports.MixerData }; }
        }
        byte IInputPort.ReadByte(int port)
        {
            switch(port)
            {
            case Ports.DspReadData:
                if(outputData.Count > 0)
                    return this.outputData.Dequeue();
                else
                    return 0;

            case Ports.DspWrite:
                return 0x00;

            case Ports.DspReadBufferStatus:
                if(this.mixer.InterruptStatusRegister == InterruptStatus.Dma8)
                    System.Diagnostics.Debug.WriteLine("Sound Blaster 8-bit DMA acknowledged");
                this.mixer.InterruptStatusRegister = InterruptStatus.None;
                return this.outputData.Count > 0 ? (byte)0x80 : (byte)0u;

            case Ports.MixerAddress:
                return (byte)this.mixer.CurrentAddress;

            case Ports.MixerData:
                return this.mixer.ReadData();
            }

            return 0;
        }
        ushort IInputPort.ReadWord(int port)
        {
            uint value = ((IInputPort)this).ReadByte(port);
            value |= (uint)(((IInputPort)this).ReadByte(port + 1) << 8);
            return (ushort)value;
        }
        #endregion

        #region IOutputPort Members
        IEnumerable<int> IOutputPort.OutputPorts
        {
            get { return new int[] { Ports.DspReset, Ports.DspWrite, Ports.MixerAddress }; }
        }
        void IOutputPort.WriteByte(int port, byte value)
        {
            switch(port)
            {
            case Ports.DspReset:
                // Expect a 1, then 0 written to reset the DSP.
                if(value == 1)
                    this.state = BlasterState.ResetRequest;
                else if(value == 0 && this.state == BlasterState.ResetRequest)
                {
                    this.state = BlasterState.Resetting;
                    Reset();
                }
                break;

            case Ports.DspWrite:
                if(this.state == BlasterState.WaitingForCommand)
                {
                    this.currentCommand = value;
                    this.state = BlasterState.ReadingCommand;
                    this.commandData.Clear();
                    commandLengths.TryGetValue(value, out this.commandDataLength);
                    if(this.commandDataLength == 0)
                        ProcessCommand();
                }
                else if(this.state == BlasterState.ReadingCommand)
                {
                    this.commandData.Add(value);
                    if(this.commandData.Count >= this.commandDataLength)
                        ProcessCommand();
                }
                break;

            case Ports.MixerAddress:
                this.mixer.CurrentAddress = value;
                break;
            }
        }
        void IOutputPort.WriteWord(int port, ushort value)
        {
            throw new NotImplementedException();
        }
        #endregion

        #region IDmaDevice8 Members
        int IDmaDevice8.Channel
        {
            get { return this.dma8; }
        }
        int IDmaDevice8.WriteBytes(IntPtr source, int count)
        {
            return this.dsp.DmaWrite(source, count);
        }
        void IDmaDevice8.SingleCycleComplete()
        {
            this.dsp.IsEnabled = false;
            RaiseInterrupt();
        }
        #endregion

        #region IDmaDevice16 Members
        int IDmaDevice16.Channel
        {
            get { return this.dma16; }
        }
        int IDmaDevice16.WriteWords(IntPtr source, int count)
        {
            throw new NotImplementedException();
        }
        void IDmaDevice16.SingleCycleComplete()
        {
            throw new NotImplementedException();
        }
        #endregion

        #region IVirtualDevice Members
        void IVirtualDevice.Pause()
        {
            this.pausePlayback = true;
        }
        void IVirtualDevice.Resume()
        {
            this.pausePlayback = false;
        }
        /// <summary>
        /// Invoked when the virtual device has been added to a VirtualMachine.
        /// </summary>
        /// <param name="vm">VirtualMachine which owns the device.</param>
        void IVirtualDevice.DeviceRegistered(VirtualMachine vm)
        {
            var env = string.Format("A220 I{0} D{1} T4", this.irq, this.dma8);
            vm.EnvironmentVariables["BLASTER"] = env;
        }
        /// <summary>
        /// Invoked when the device should save its current state to a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="destination"><see cref="System.IO.Stream"/> to which the state is saved.</param>
        void IVirtualDevice.SaveCurrentState(Stream destination)
        {
        }
        /// <summary>
        /// Invoked when the device should restore its state from a <see cref="System.IO.Stream"/>.
        /// </summary>
        /// <param name="source"><see cref="System.IO.Stream"/> from which the state is restored.</param>
        void IVirtualDevice.RestoreState(Stream source)
        {
        }
        #endregion

        #region IDisposable Members
        /// <summary>
        /// Releases resources used by the SoundBlaster instance.
        /// </summary>
        public void Dispose()
        {
            if(this.playbackThread != null && this.playbackThread.IsAlive)
                this.playbackThread.Abort();
        }
        #endregion

        #region Public Methods
        /// <summary>
        /// Returns the current playback position indicatiors.
        /// </summary>
        /// <param name="writePos">Current position of the write pointer.</param>
        /// <param name="playPos">Current position of the playback pointer.</param>
        public void GetBufferPositions(out int writePos, out int playPos)
        {
            var buffer = this.currentSoundBuffer;
            if(buffer != null)
                buffer.GetPositions(out writePos, out playPos);
            else
            {
                writePos = 0;
                playPos = 0;
            }
        }
        #endregion

        #region Private Methods
        /// <summary>
        /// Raises a hardware interrupt and prepares for an acknowledge response.
        /// </summary>
        private void RaiseInterrupt()
        {
            this.mixer.InterruptStatusRegister = InterruptStatus.Dma8;
            this.vm.InterruptController.RaiseHardwareInterrupt(this.irq);
            System.Diagnostics.Debug.WriteLine("Sound Blaster IRQ");
        }
        /// <summary>
        /// Resets the DSP.
        /// </summary>
        private void Reset()
        {
            outputData.Clear();
            outputData.Enqueue(0xAA);
            this.state = BlasterState.WaitingForCommand;
            this.blockTransferSizeSet = false;
            this.dsp.Reset();
        }
        /// <summary>
        /// Performs the action associated with the current DSP command.
        /// </summary>
        private void ProcessCommand()
        {
            outputData.Clear();
            switch(currentCommand)
            {
            case Commands.GetVersionNumber:
                outputData.Enqueue(4);
                outputData.Enqueue(5);
                break;

            case Commands.DspIdentification:
                outputData.Enqueue((byte)~commandData[0]);
                break;

            case Commands.SetTimeConstant:
                dsp.SampleRate = 256000000 / (65536 - (commandData[0] << 8));
                break;

            case Commands.SetSampleRate:
                dsp.SampleRate = (commandData[0] << 8) | commandData[1];
                break;

            case Commands.SetBlockTransferSize:
                dsp.BlockTransferSize = (commandData[0] | (commandData[1] << 8)) + 1;
                this.blockTransferSizeSet = true;
                break;

            case Commands.SingleCycleDmaOutput8:
            case Commands.HighSpeedSingleCycleDmaOutput8:
            case Commands.SingleCycleDmaOutput8_Alt:
            case Commands.SingleCycleDmaOutput8Fifo_Alt:
                //if(commandData.Count >= 2 && (commandData[0] | (commandData[1] << 8)) >= 2048)
                    dsp.Begin(false, false, false);
                //else
                //    this.vm.InterruptController.RaiseHardwareInterrupt(this.irq);
                System.Diagnostics.Debug.WriteLine("Single-cycle DMA");
                vm.PerformDmaTransfers();
                break;

            case Commands.SingleCycleDmaOutputADPCM4Ref:
                dsp.Begin(false, false, false, CompressionLevel.ADPCM4, true);
                System.Diagnostics.Debug.WriteLine("Single-cycle DMA ADPCM4 with reference byte");
                vm.PerformDmaTransfers();
                break;

            case Commands.SingleCycleDmaOutputADPCM4:
                dsp.Begin(false, false, false, CompressionLevel.ADPCM4, false);
                System.Diagnostics.Debug.WriteLine("Single-cycle DMA ADPCM4");
                vm.PerformDmaTransfers();
                break;

            case Commands.SingleCycleDmaOutputADPCM2Ref:
                dsp.Begin(false, false, false, CompressionLevel.ADPCM2, true);
                System.Diagnostics.Debug.WriteLine("Single-cycle DMA ADPCM2 with reference byte");
                vm.PerformDmaTransfers();
                break;

            case Commands.SingleCycleDmaOutputADPCM2:
                dsp.Begin(false, false, false, CompressionLevel.ADPCM2, false);
                System.Diagnostics.Debug.WriteLine("Single-cycle DMA ADPCM2");
                vm.PerformDmaTransfers();
                break;

            case Commands.SingleCycleDmaOutputADPCM3Ref:
                dsp.Begin(false, false, false, CompressionLevel.ADPCM3, true);
                System.Diagnostics.Debug.WriteLine("Single-cycle DMA ADPCM3 with reference byte");
                vm.PerformDmaTransfers();
                break;

            case Commands.SingleCycleDmaOutputADPCM3:
                dsp.Begin(false, false, false, CompressionLevel.ADPCM3, false);
                System.Diagnostics.Debug.WriteLine("Single-cycle DMA ADPCM3");
                vm.PerformDmaTransfers();
                break;

            case Commands.AutoInitDmaOutput8:
            case Commands.HighSpeedAutoInitDmaOutput8:
                if(!this.blockTransferSizeSet)
                    dsp.BlockTransferSize = ((commandData[1] | (commandData[2] << 8)) + 1);
                this.dsp.Begin(false, false, true);
                System.Diagnostics.Debug.WriteLine("Auto-init DMA");
                break;

            case Commands.AutoInitDmaOutput8_Alt:
            case Commands.AutoInitDmaOutput8Fifo_Alt:
                if(!this.blockTransferSizeSet)
                    dsp.BlockTransferSize = ((commandData[1] | (commandData[2] << 8)) + 1);
                this.dsp.Begin(false, (commandData[0] & (1 << 5)) != 0, true);
                System.Diagnostics.Debug.WriteLine("Auto-init DMA");
                break;

            case Commands.ExitAutoInit8:
                this.dsp.ExitAutoInit();
                break;

            case Commands.SingleCycleDmaOutput16:
            case Commands.SingleCycleDmaOutput16Fifo:
                this.dsp.Begin(true, (commandData[0] & (1 << 5)) != 0, false);
                System.Diagnostics.Debug.WriteLine("Single-cycle DMA");
                break;

            case Commands.AutoInitDmaOutput16:
            case Commands.AutoInitDmaOutput16Fifo:
                System.Diagnostics.Debug.WriteLine("Auto-init DMA");
                this.dsp.Begin(true, (commandData[0] & (1 << 5)) != 0, true);
                break;

            case Commands.TurnOnSpeaker:
                break;

            case Commands.TurnOffSpeaker:
                break;

            case Commands.PauseDmaMode:
            case Commands.PauseDmaMode16:
            case Commands.ExitDmaMode16:
                this.dmaChannel.IsActive = false;
                this.dsp.IsEnabled = false;
                System.Diagnostics.Debug.WriteLine("Pause Sound Blaster DMA");
                break;

            case Commands.ContinueDmaMode:
            case Commands.ContinueDmaMode16:
                this.dmaChannel.IsActive = true;
                this.dsp.IsEnabled = true;
                System.Diagnostics.Debug.WriteLine("Continue Sound Blaster DMA");
                break;

            case Commands.RaiseIrq8:
                RaiseInterrupt();
                break;

            case Commands.SetInputSampleRate:
                // Ignore for now.
                break;

            case Commands.PauseForDuration:
                this.pauseDuration = commandData[0] | (commandData[1] << 8);
                break;

            default:
                throw new NotImplementedException(string.Format("Sound Blaster command {0:X2}h not implemented.", currentCommand));
            }

            this.state = BlasterState.WaitingForCommand;
        }

        private DirectSoundBuffer currentSoundBuffer;

        /// <summary>
        /// Polls the PlaybackEngine instance.
        /// </summary>
        private void AudioPlayback()
        {
            byte[] buffer = new byte[512];
            short[] writeBuffer = new short[65536 * 2];

            using(DirectSoundBuffer soundBuffer = DirectSound.GetInstance(hwnd).CreateBuffer(44100, ChannelMode.Stereo, BitsPerSample.Sixteen, TimeSpan.FromSeconds(0.25)))
            {
                this.currentSoundBuffer = soundBuffer;
                soundBuffer.Play(PlaybackMode.LoopContinuously);

                while(true)
                {
                    dsp.Read(buffer, 0, buffer.Length);
                    int length;
                    if(this.dsp.Is16Bit && this.dsp.IsStereo)
                        length = Resample16Stereo(dsp.SampleRate, 44100, buffer, writeBuffer);
                    else if(this.dsp.Is16Bit)
                        length = Resample16Monaural(dsp.SampleRate, 44100, buffer, writeBuffer);
                    else if(this.dsp.IsStereo)
                        length = Resample8Stereo(dsp.SampleRate, 44100, buffer, writeBuffer);
                    else
                        length = Resample8Monaural(dsp.SampleRate, 44100, buffer, writeBuffer);

                    while(!soundBuffer.Write(writeBuffer, 0, length))
                    {
                        Thread.Sleep(1);
                    }

                    if(this.pausePlayback)
                    {
                        soundBuffer.Stop();
                        while(this.pausePlayback)
                        {
                            Thread.Sleep(1);
                        }

                        soundBuffer.Play(PlaybackMode.LoopContinuously);
                    }

                    if(this.pauseDuration > 0)
                    {
                        Array.Clear(writeBuffer, 0, writeBuffer.Length);
                        int count = this.pauseDuration / (1024 / 2) + 1;
                        for(int i = 0; i < count; i++)
                        {
                            while(!soundBuffer.Write(writeBuffer, 0, 1024))
                            {
                                Thread.Sleep(1);
                            }
                        }

                        this.pauseDuration = 0;
                        RaiseInterrupt();
                    }
                }
            }
        }
        /// <summary>
        /// Converts a chunk of 8-bit monaural sound data to 16-bit stereo sound data.
        /// </summary>
        /// <param name="sourceRate">Source sampling rate.</param>
        /// <param name="destRate">Destination sampling rate.</param>
        /// <param name="source">Source data.</param>
        /// <param name="dest">Destination data.</param>
        /// <returns>Size of destination data in samples.</returns>
        private int Resample8Monaural(int sourceRate, int destRate, byte[] source, short[] dest)
        {
            double src2Dest = (double)destRate / (double)sourceRate;
            double dest2Src = (double)sourceRate / (double)destRate;

            int length = (int)(src2Dest * source.Length);

            for(int i = 0; i < length; i++)
            {
                short value = (short)(((int)source[(int)(i * dest2Src)] - 128) << 8);
                dest[(i << 1)] = value;
                dest[(i << 1) + 1] = value;
            }

            return length * 2;
        }
        /// <summary>
        /// Converts a chunk of 8-bit stereo sound data to 16-bit stereo sound data.
        /// </summary>
        /// <param name="sourceRate">Source sampling rate.</param>
        /// <param name="destRate">Destination sampling rate.</param>
        /// <param name="source">Source data.</param>
        /// <param name="dest">Destination data.</param>
        /// <returns>Size of destination data in samples.</returns>
        private int Resample8Stereo(int sourceRate, int destRate, byte[] source, short[] dest)
        {
            double src2Dest = (double)destRate / (double)sourceRate;
            double dest2Src = (double)sourceRate / (double)destRate;

            int length = (int)(src2Dest * source.Length) / 2;

            for(int i = 0; i < length; i++)
            {
                int srcIndex = (int)(i * dest2Src) << 1;
                dest[(i << 1)] = (short)(((int)source[srcIndex] - 128) << 8);
                dest[(i << 1) + 1] = (short)(((int)source[srcIndex + 1] - 128) << 8);
            }

            return length * 2;
        }
        /// <summary>
        /// Converts a chunk of 16-bit monaural sound data to 16-bit stereo sound data.
        /// </summary>
        /// <param name="sourceRate">Source sampling rate.</param>
        /// <param name="destRate">Destination sampling rate.</param>
        /// <param name="source">Source data.</param>
        /// <param name="dest">Destination data.</param>
        /// <returns>Size of destination data in samples.</returns>
        private int Resample16Monaural(int sourceRate, int destRate, byte[] source, short[] dest)
        {
            double src2Dest = (double)destRate / (double)sourceRate;
            double dest2Src = (double)sourceRate / (double)destRate;

            int length = (int)(src2Dest * source.Length) / 2;

            for(int i = 0; i < length; i++)
            {
                int srcIndex = (int)(i * dest2Src) << 1;
                short value = (short)(ushort)(source[srcIndex] | (source[srcIndex + 1] << 8));
                dest[(i << 1)] = value;
                dest[(i << 1) + 1] = value;
            }

            return length * 2;
        }
        /// <summary>
        /// Converts a chunk of 16-bit stereo sound data to 16-bit stereo sound data.
        /// </summary>
        /// <param name="sourceRate">Source sampling rate.</param>
        /// <param name="destRate">Destination sampling rate.</param>
        /// <param name="source">Source data.</param>
        /// <param name="dest">Destination data.</param>
        /// <returns>Size of destination data in samples.</returns>
        private int Resample16Stereo(int sourceRate, int destRate, byte[] source, short[] dest)
        {
            double src2Dest = (double)destRate / (double)sourceRate;
            double dest2Src = (double)sourceRate / (double)destRate;

            int length = (int)(src2Dest * source.Length) / 4;

            for(int i = 0; i < length; i++)
            {
                int srcIndex = (int)(i * dest2Src) << 2;
                dest[(i << 1)] = (short)(ushort)(source[srcIndex] | (source[srcIndex + 1] << 8));
                dest[(i << 1) + 1] = (short)(ushort)(source[srcIndex + 2] | (source[srcIndex + 3] << 8));
            }

            return length * 2;
        }
        #endregion

        #region Private Fields
        private readonly VirtualMachine vm;
        private readonly IntPtr hwnd;
        private readonly DmaChannel dmaChannel;
        private readonly List<byte> commandData = new List<byte>();
        private readonly Queue<byte> outputData = new Queue<byte>();
        private readonly int irq;
        private readonly int dma8;
        private readonly int dma16;
        private readonly Thread playbackThread;
        private readonly Dsp dsp;
        private readonly Mixer mixer;
        private volatile bool pausePlayback;
        private byte currentCommand;
        private byte commandDataLength;
        private BlasterState state;
        private bool blockTransferSizeSet;
        private int pauseDuration;
        #endregion

        #region Private Static Fields
        private static readonly Dictionary<byte, byte> commandLengths = new Dictionary<byte, byte>();
        #endregion
    }

    /// <summary>
    /// Specifies the current state of the DSP command processor.
    /// </summary>
    internal enum BlasterState
    {
        /// <summary>
        /// The DSP is ready to receive a command.
        /// </summary>
        WaitingForCommand,
        /// <summary>
        /// The DSP is waiting for all of a command's parameters to be written.
        /// </summary>
        ReadingCommand,
        /// <summary>
        /// A one has been written to the reset port.
        /// </summary>
        ResetRequest,
        /// <summary>
        /// The reset port has changed from 1 to 0 and the DSP is resetting.
        /// </summary>
        Resetting
    }
}
